﻿using System;
using Velocity4Net.Errors;
using Velocity4Net.Util;

namespace Velocity4Net.Runtime.Directives
{

    /*
     * Licensed to the Apache Software Foundation (ASF) under one
     * or more contributor license agreements.  See the NOTICE file
     * distributed with this work for additional information
     * regarding copyright ownership.  The ASF licenses this file
     * to you under the Apache License, Version 2.0 (the
     * "License"); you may not use this file except in compliance
     * with the License.  You may obtain a copy of the License at
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing,
     * software distributed under the License is distributed on an
     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     * KIND, either express or implied.  See the License for the
     * specific language governing permissions and limitations
     * under the License.
     */

























    /**
     * This class acts as a proxy for potential macros.  When the AST is built
     * this class is inserted as a placeholder for the macro (whether or not
     * the macro is actually defined).  At render time we check whether there is
     * a implementation for the macro call. If an implementation cannot be
     * found the literal text is rendered.
     * @since 1.6
     */
    public class RuntimeMacro : Directive
    {
        /**
         * Name of the macro
         */
        private String macroName;

        /**
         * Literal text of the macro
         */
        private String literal = null;

        /**
         * Node of the macro call
         */
        private Node node = null;

        /**
         * Indicates if we are running in strict reference mode.
         */
        protected bool strictRef = false;

        /**
         * badArgsErrorMsg will be non null if the arguments to this macro
         * are deamed bad at init time, see the init method.  If his is non null, then this macro
         * cannot be rendered, and if there is an attempt to render we throw an exception
         * with this as the message.
         */
        private String badArgsErrorMsg = null;

        /**
         * Return name of this Velocimacro.
         *
         * @return The name of this Velocimacro.
         */
        public String getName()
        {
            return macroName;
        }

        /**
         * Override to always return "macro".  We don't want to use
         * the macro name here, since when writing VTL that uses the
         * scope, we are within a #macro call.  The macro name will instead
         * be used as the scope name when defining the body of a BlockMacro.
         */
        public String getScopeName()
        {
            return "macro";
        }

        /**
         * Velocimacros are always LINE
         * type directives.
         *
         * @return The type of this directive.
         */
        public int getType()
        {
            return LINE;
        }


        /**
         * Initialize the Runtime macro. At the init time no implementation so we
         * just save the values to use at the render time.
         *
         * @param rs runtime services
         * @param name macro name
         * @param context InternalContextAdapter
         * @param node node containing the macro call
         */
        public void init(RuntimeServices rs, String name, InternalContextAdapter context,
                         Node node)
        {
            super.init(rs, context, node);

            macroName = Validate.notNull(name);
            macroName = rsvc.useStringInterning() ? macroName.intern() : macroName;
            this.node = node;

            /**
             * Apply strictRef setting only if this really looks like a macro,
             * so strict mode doesn't balk at things like #E0E0E0 in a template.
             * compare with ")" is a simple #foo() style macro, comparing to
             * "#end" is a block style macro. We use starts with because the token
             * may end with '\n'
             */
            // Tokens can be used here since we are in init() and Tokens have not been dropped yet
            Token t = node.getLastToken();
            if (t.image.startsWith(")") || t.image.startsWith(rsvc.getParserConfiguration().getHashChar() + "end"))
            {
                strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
            }

            // Validate that none of the arguments are plain words, (VELOCITY-614)
            // they should be string literals, references, inline maps, or inline lists
            for (int n = 0; n < node.jjtGetNumChildren(); n++)
            {
                Node child = node.jjtGetChild(n);
                if (child.getType() == ParserTreeConstants.JJTWORD)
                {
                    badArgsErrorMsg = "Invalid arg '" + child.getFirstTokenImage()
                    + "' in macro #" + macroName + " at " + StringUtils.formatFileString(child);

                    if (strictRef)  // If strict, throw now
                    {
                        /* indicate col/line assuming it starts at 0
                         * this will be corrected one call up  */
                        throw new TemplateInitException(badArgsErrorMsg,
                        null,
                        rsvc.getLogContext().getStackTrace(),
                        context.getCurrentTemplateName(), 0, 0);
                    }
                }
            }
            // TODO: Improve this
            // this is only needed if the macro does not exist during runtime
            // since tokens are eliminated after this init call, we have to create a cached version of the
            // literal which is in 99.9% cases waste. However, for regular macro calls (non Block macros)
            // this doesn't create very long Strings so it's probably acceptable
            getLiteral();
        }

        /**
         * It is probably quite rare that we need to render the macro literal
         * but since we won't keep the tokens in memory, we need to calculate it
         * at parsing time.
         */
        private String getLiteral()
        {
            SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
            ASTDirective directive = (ASTDirective)node;

            String morePrefix = directive.getMorePrefix();

            if (literal == null)
            {
                StringBuilder buffer = new StringBuilder();
                Token t = node.getFirstToken();

                /* avoid outputting twice the prefix and the 'MORE' prefix,
                 * but still display the prefix in the cases where the ASTDirective would hide it */
                int pos = -1;
                while (t != null && t != node.getLastToken())
                {
                    if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
                    if (pos != -1)
                    {
                        buffer.Append(t.image.substring(pos));
                        pos = 0;
                    }
                    else if (morePrefix.Length == 0 && spaceGobbling.compareTo(SpaceGobbling.LINES) >= 0)
                    {
                        buffer.Append(t.image);
                    }
                    t = t.next;
                }

                if (t != null)
                {
                    if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
                    if (pos != -1)
                    {
                        buffer.Append(t.image.substring(pos));
                    }
                }

                literal = buffer.ToString();
                /* avoid outputting twice the postfix, but still display it in the cases
                 * where the ASTDirective would hide it */
                String postfix = directive.getPostfix();
                if ((morePrefix.Length > 0 || spaceGobbling == SpaceGobbling.NONE) && literal.endsWith(postfix))
                {
                    literal = literal.substring(0, literal.Length - postfix.Length);
                }
            }
            return literal;
        }


        /**
         * Velocimacro implementation is not known at the init time. So look for
         * a implementation in the macro libraries and if finds one renders it. The
         * actual rendering is delegated to the VelocimacroProxy object. When
         * looking for a macro we first loot at the template with has the
         * macro call then we look at the macro libraries in the order they appear
         * in the list. If a macro has many definitions above look up will
         * determine the precedence.
         *
         * @param context
         * @param writer
         * @param node
         * @return true if the rendering is successful
         * @throws IOException
         * @throws ResourceNotFoundException
         * @throws ParseErrorException
         * @throws MethodInvocationException
         */
        public bool render(InternalContextAdapter context, Writer writer,
                              Node node)
        {
            return render(context, writer, node, null);
        }

        /**
         * This method is used with BlockMacro when we want to render a macro with a body AST.
         *
         * @param context
         * @param writer
         * @param node
         * @param body AST block that was enclosed in the macro body.
         * @return true if the rendering is successful
         * @throws IOException
         * @throws ResourceNotFoundException
         * @throws ParseErrorException
         * @throws MethodInvocationException
         */
        public bool render(InternalContextAdapter context, Writer writer,
                              Node node, Renderable body)
        {
            VelocimacroProxy vmProxy = null;
            Template renderingTemplate = (Template)context.getCurrentResource();

            /**
             * first look in the source template
             */
            Object o = rsvc.getVelocimacro(macroName, renderingTemplate, getTemplate());

            if (o != null)
            {
                // getVelocimacro can only return a VelocimacroProxy so we don't need the
                // costly is check
                vmProxy = (VelocimacroProxy)o;
            }

            /**
             * if not found, look in the macro libraries.
             */
            if (vmProxy == null)
            {
                List macroLibraries = context.getMacroLibraries();
                if (macroLibraries != null)
                {
                    for (int i = macroLibraries.Count - 1; i >= 0; i--)
                    {
                        o = rsvc.getVelocimacro(macroName, renderingTemplate, (Template)macroLibraries.get(i));

                        // get the first matching macro
                        if (o != null)
                        {
                            vmProxy = (VelocimacroProxy)o;
                            break;
                        }
                    }
                }
            }

            if (vmProxy != null)
            {
                if (badArgsErrorMsg != null)
                {
                    throw new TemplateInitException(badArgsErrorMsg,
                      null, rsvc.getLogContext().getStackTrace(),
                      context.getCurrentTemplateName(), node.getColumn(), node.getLine());
                }

                try
                {
                    preRender(context);
                    return vmProxy.render(context, writer, node, body);
                }
                catch (StopCommand stop)
                {
                    if (!stop.isFor(this))
                    {
                        throw stop;
                    }
                    return true;
                }
                catch (Exception e)
                {
                    /**
                     * We catch, the exception here so that we can record in
                     * the logs the template and line number of the macro call
                     * which generate the exception.  This information is
                     * especially important for multiple macro call levels.
                     * this is also true for the following catch blocks.
                     */
                    log.Error($"Exception in macro #{macroName} called at {StringUtils.formatFileString(node)}");
                    throw e;
                }
                finally
                {
                    postRender(context);
                }
            }
            else if (strictRef)
            {
                throw new VelocityException("Macro '#" + macroName + "' is not defined at "
                    + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace());
            }

            /**
             * If we cannot find an implementation write the literal text
             */
            writer.write(getLiteral());
            return true;
        }
    }
}